﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Reflection;
using System.Reflection.Emit;

namespace CloneMachine
{
    public class Clone
    {
        private LocalBuilder _lbfTemp;

        /// <summary>
        /// Delegate handler that's used to compile the IL to.
        /// (This delegate is standard in .net 3.5)
        /// </summary>
        /// <typeparam name="T1">Parameter Type</typeparam>
        /// <typeparam name="TResult">Return Type</typeparam>
        /// <param name="arg1">Argument</param>
        /// <returns>Result</returns>
        private delegate TResult Func<T1, TResult>(T1 arg1);
        /// <summary>
        /// This dictionary caches the delegates for each 'to-clone' type.
        /// </summary>
        private static Dictionary<Type, Delegate> cachedIL = new Dictionary<Type, Delegate>();

        /// <summary>
        /// Generic cloning method that clones an object using IL.
        /// Only the first call of a certain type will hold back performance.
        /// After the first call, the compiled IL is executed.
        /// </summary>
        /// <typeparam name="T">Type of object to clone</typeparam>
        /// <param name="myObject">Object to clone</param>
        /// <returns>Cloned object</returns>
        public T CloneWithIL<T>(T obj)
        {
            Delegate myExec = null;
            if (!cachedIL.TryGetValue(typeof(T), out myExec))
            {
                //Creates an IL generator.
                DynamicMethod dynMethod = new DynamicMethod("DoClone", typeof(T), new Type[] { typeof(T) }, true);
                ConstructorInfo cInfo = obj.GetType().GetConstructors()[0];// GetConstructor(new Type[] { });

                ILGenerator generator = dynMethod.GetILGenerator();

                LocalBuilder lbf = generator.DeclareLocal(typeof(T));

                generator.Emit(OpCodes.Newobj, cInfo);
                generator.Emit(OpCodes.Stloc_0);
                foreach (FieldInfo field in obj.GetType().GetFields(BindingFlags.Instance | BindingFlags.NonPublic | BindingFlags.Public))
                {
                    //DEEP COPY
                    if (field.FieldType.IsValueType || field.FieldType == typeof(string))
                        CopyValueType(generator, field);
                    else if (field.FieldType.IsClass)
                        CopyReferenceType(generator, field);
                }

                // Load new constructed obj on eval stack -> 1 item on stack
                generator.Emit(OpCodes.Ldloc_0);
                // Return constructed object.   --> 0 items on stack
                generator.Emit(OpCodes.Ret);

                myExec = dynMethod.CreateDelegate(typeof(Func<T, T>));
                cachedIL.Add(typeof(T), myExec);
            }
            return ((Func<T, T>)myExec)(obj);
        }

        private void CopyValueType(ILGenerator generator, FieldInfo field)
        {
            // Load the new object on the eval stack... (currently 1 item on eval stack)
            generator.Emit(OpCodes.Ldloc_0);
            // Load initial object (parameter)          (currently 2 items on eval stack)
            generator.Emit(OpCodes.Ldarg_0);
            // Replace value by field value             (still currently 2 items on eval stack)
            generator.Emit(OpCodes.Ldfld, field);
            // Store the value of the top on the eval stack into the object underneath that value on the value stack.
            //  (0 items on eval stack)
            generator.Emit(OpCodes.Stfld, field);
        }

        private void CopyReferenceType(ILGenerator generator, FieldInfo field)
        {
            // We have a reference type.
            _lbfTemp = generator.DeclareLocal(field.FieldType);
            if (field.FieldType.GetInterface("IEnumerable") != null)
            {
                // We have a list type (generic).
                if (field.FieldType.IsGenericType)
                {
                    // Get argument of list type
                    Type argType = field.FieldType.GetGenericArguments()[0];
                    // Check that it has a constructor that accepts another IEnumerable.
                    Type genericType = Type.GetType("System.Collections.Generic.IEnumerable`1["
                            + argType.FullName + "]");

                    ConstructorInfo ci = field.FieldType.GetConstructor(new Type[] { genericType });
                    if (ci != null)
                    {
                        // It has! (Like the List<> class)
                        generator.Emit(OpCodes.Ldarg_0);
                        generator.Emit(OpCodes.Ldfld, field);
                        generator.Emit(OpCodes.Newobj, ci);
                        generator.Emit(OpCodes.Stloc, _lbfTemp);
                        PlaceNewTempObjInClone(generator, field);
                    }
                }
            }
            else
            {
                CreateNewTempObject(generator, field.FieldType);
                PlaceNewTempObjInClone(generator, field);
                foreach (FieldInfo fi in field.FieldType.GetFields(System.Reflection.BindingFlags.Instance
                    | System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Public))
                {
                    if (fi.FieldType.IsValueType || fi.FieldType == typeof(string))
                        CopyValueTypeTemp(generator, field, fi);
                    else if (fi.FieldType.IsClass)
                        CopyReferenceType(generator, fi);
                }
            }
        }

        private void CreateNewTempObject(ILGenerator generator, Type type)
        {
            ConstructorInfo cInfo = type.GetConstructor(new Type[] { });
            generator.Emit(OpCodes.Newobj, cInfo);
            generator.Emit(OpCodes.Stloc, _lbfTemp);
        }

        private void CopyValueTypeTemp(ILGenerator generator, FieldInfo fieldParent, FieldInfo fieldDetail)
        {
            generator.Emit(OpCodes.Ldloc_1);
            generator.Emit(OpCodes.Ldarg_0);
            generator.Emit(OpCodes.Ldfld, fieldParent);
            generator.Emit(OpCodes.Ldfld, fieldDetail);
            generator.Emit(OpCodes.Stfld, fieldDetail);
        }

        private void PlaceNewTempObjInClone(ILGenerator generator, FieldInfo field)
        {
            // Get object from custom location and store it in right field of location 0
            generator.Emit(OpCodes.Ldloc_0);
            generator.Emit(OpCodes.Ldloc, _lbfTemp);
            generator.Emit(OpCodes.Stfld, field);
        }
    }
}